﻿// -------------------------------------------------------------------------------------------------------
// LICENSE INFORMATION
//
// - This software is licensed under the MIT shared source license.
// - The "official" source code for this project is maintained at http://mtconnect.codeplex.com
//
// Copyright (c) 2010 OpenNETCF Consulting
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 
// associated documentation files (the "Software"), to deal in the Software without restriction, 
// including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 
// and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, 
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial 
// portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT 
// NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 
// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 
// -------------------------------------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using OpenNETCF.Web;
using OpenNETCF.IoC;
using System.Xml.Linq;
using System.Diagnostics;
using System.IO;

namespace OpenNETCF.DeviceAdmin
{
    public class FileSystemHandler : IHttpHandler
    {
        private enum TargetType
        {
            Directory,
            File
        }

        public string FileSystemRoot = "/filesystem";

        public bool IsReusable
        {
            get { return true; }
        }

        public void ProcessRequest(HttpContext context)
        {
            var et = Environment.TickCount;

            switch (context.Request.RequestType.ToUpper())
            {
                case "GET":
                    HandleGet(context);
                    break;
                case "PUT":
                    HandlePut(context);
                    break;
                case "DELETE":
                    HandleDelete(context);
                    break;
            }

            et = Environment.TickCount - et;

            Debug.WriteLineIf((et > 100), string.Format("FileSystemHandler took {0}ms at {1}", et, context.Request.Path));
        }

        private string GetRequestData(HttpContext context)
        {
            var buffer = new byte[context.Request.InputStream.Length];
            context.Request.InputStream.Read(buffer, 0, buffer.Length);
            return Encoding.UTF8.GetString(buffer);
        }

        private string GetTargetPath(HttpContext context)
        {
            var path = context.Request.Path;
            var index = path.IndexOf(FileSystemRoot, 0, StringComparison.InvariantCultureIgnoreCase);

            if (!FileSystemRoot.EndsWith("/"))
            {
                // skip past the '/' path delimiter
                index++;
            }

            if (index < 0)
            {
                throw new Exception("Bad path: " + path);
            }

            return SanitizePath(path.Substring(index + FileSystemRoot.Length));
        }

        private string SanitizePath(string path)
        {
            return Uri.UnescapeDataString(path);
        }

        private void HandleDelete(HttpContext context)
        {
            // determine path
            var path = GetTargetPath(context);

            // are we looking for a directory or file?
            var targetType = GetTargetType(context);

            switch (targetType)
            {
                case TargetType.Directory:
                    DeleteDirectory(path);
                    break;
                case TargetType.File:
                    DeleteFile(path);
                    break;
            }
        }

        private void HandlePut(HttpContext context)
        {
            // determine path
            var path = GetTargetPath(context);

            // what are we putting?
            var requestata = GetRequestData(context);
            var doc = XDocument.Parse(requestata);

            var element = doc.Elements().First();
            if (element == null) throw new Exception("Missing data element");

            if (element.Name == "Directory")
            {
                CreateDirectory(path);
            }
            else if (element.Name == "File")
            {
                CreateFile(path, element);
            }
            else
            {
                throw new NotSupportedException("Unsupported element: " + element.Name);
            }
        }

        private void CreateFile(string path, XElement element)
        {
            // determine data type
            var type = element.Attribute("type").Value;
            
            if (type == "text")
            {
                FileInfo fi = new FileInfo(path);
                if (fi.Exists) fi.Delete();

                var encName = element.Attribute("encoding").Value;
                var encoding = Encoding.GetEncoding(encName);

                using (var stream = fi.Create())
                using(var writer = new StreamWriter(stream, encoding))
                {
                    writer.Write(element.Value);
                }
            }
            else if (type == "binary")
            {
                FileInfo fi = new FileInfo(path);
                if (fi.Exists) fi.Delete();

                using (var stream = fi.Create())
                {
                    var buffer = Convert.FromBase64String(element.Value);
                    stream.Write(buffer, 0, buffer.Length);
                }
            }
            else
            {
                throw new NotSupportedException("Unsupported data type: " + type);
            }
        }

        private void DeleteFile(string path)
        {
            System.IO.FileInfo fi = new System.IO.FileInfo(path);
            if (!fi.Exists) return;

            // TODO: handle errors (i.e. read-only, etc)
            fi.Delete();
        }

        private void DeleteDirectory(string path)
        {
            System.IO.DirectoryInfo di = new System.IO.DirectoryInfo(path);
            if (!di.Exists) return;

            // TODO: handle errors (i.e. dir not empty)
            di.Delete();
        }

        private void CreateDirectory(string path)
        {
            System.IO.DirectoryInfo di = new System.IO.DirectoryInfo(path);
            if (di.Exists) return;
            di.Create();
        }

        private TargetType GetTargetType(HttpContext context)
        {
            var typeString = context.Request.QueryString["type"];
            if (typeString == null)
            {
                throw new Exception("Missing 'type'. Expected 'd' or 'f'");
            }
            typeString = typeString.ToLower();
            switch (typeString)
            {
                case "d":
                    return TargetType.Directory;
                case "f":
                    return TargetType.File;
                default:
                    throw new Exception("Invalid 'type'. Expected 'd' or 'f'");
            }
        }

        private void HandleGet(HttpContext context)
        {
            // determine path
            var path = GetTargetPath(context);

            // are we looking for a directory or file?
            var targetType = GetTargetType(context);

            // do we want the item, or just the existence check?
            var command = context.Request.QueryString["cmd"];
            if (command != null)
            {
                command = command.ToLower();

                switch (command)
                {
                    case "attr":
                        // return target attributes
                        switch (targetType)
                        {
                            case TargetType.Directory:
                                ReturnDirectoryAttributes(context, path);
                                break;
                            case TargetType.File:
                                ReturnFileAttributes(context, path);
                                break;
                        }
                        break;
                    case "files":
                        if (targetType == TargetType.File)
                        {
                            throw new Exception("Invalid command for files: " + command);
                        }

                        ReturnFileList(context, path);
                        break;
                    case "directories":
                        if (targetType == TargetType.File)
                        {
                            throw new Exception("Invalid command for files: " + command);
                        }

                        ReturnDirectoryList(context, path);
                        break;
                    default:
                        throw new Exception("Unknown command: " + command);
                }
            }
            else
            {
                // get the target
                switch (targetType)
                {
                    case TargetType.Directory:
                        ReturnDirectoryInfo(context, path, true, true);
                        break;
                    case TargetType.File:
                        var encodingName = context.Request.QueryString["encoding"];
                        Encoding encoding = null;
                        if (encodingName != null)
                        {
                            encoding = Encoding.GetEncoding(encodingName);
                        }
                        ReturnFileContents(context, path, encoding);
                        break;
                }
            }

        }

        private void ReturnFileContents(HttpContext context, string path, Encoding encoding)
        {
            var fi = new System.IO.FileInfo(path);

            var doc = new XDocument(new XDeclaration("1.0", "UTF-8", "yes"));
            var element = fi.ToXElement();

            if (fi.Exists)
            {
                if (encoding == null)
                {
                    byte[] buffer;

                    using (var stream = fi.OpenRead())
                    {
                        buffer = new byte[stream.Length];
                        stream.Read(buffer, 0, buffer.Length);
                    }

                    element.Value = Convert.ToBase64String(buffer, 0, buffer.Length);
                }
                else
                {
                    using (var stream = fi.OpenRead())
                    using(var reader = new StreamReader(stream, encoding))
                    {
                        element.Value = reader.ReadToEnd();
                    }
                }
            }

            doc.Add(element);

            context.Response.ContentType = "text/xml";
            context.Response.Write(doc.ToString(true));
        }

        private void ReturnDirectoryList(HttpContext context, string path)
        {
            ReturnDirectoryInfo(context, path, false, true);
        }

        private void ReturnFileList(HttpContext context, string path)
        {
            ReturnDirectoryInfo(context, path, true, false);
        }

        private void ReturnDirectoryAttributes(HttpContext context, string path)
        {
            ReturnDirectoryInfo(context, path, false, false);
        }

        private void ReturnDirectoryInfo(HttpContext context, string path, bool includeFiles, bool includeDirectories)
        {
            System.IO.DirectoryInfo di;

#if !WindowsCE
            if (path == "")
            {
                di = new System.IO.DirectoryInfo(Environment.CurrentDirectory);
            }
            else
            {
                di = new System.IO.DirectoryInfo(path);
            }
#else
            di = new System.IO.DirectoryInfo(path);
#endif

            var doc = new XDocument(new XDeclaration("1.0", "UTF-8", "yes"));

            var element = di.ToXElement();

            if (di.Exists)
            {
                if (includeDirectories)
                {
                    foreach (var dir in di.GetDirectories())
                    {
                        element.Add(dir.ToXElement());
                    }
                }

                if (includeFiles)
                {
                    foreach (var file in di.GetFiles())
                    {
                        element.Add(file.ToXElement());
                    }
                }
            }

            doc.Add(element);

            context.Response.ContentType = "text/xml";
            context.Response.Write(doc.ToString(true));
        }

        private void ReturnFileAttributes(HttpContext context, string path)
        {
            System.IO.FileInfo fi = new System.IO.FileInfo(path);

            var doc = new XDocument(new XDeclaration("1.0", "UTF-8", "yes"),
                fi.ToXElement()
                );

            context.Response.ContentType = "text/xml";
            context.Response.Write(doc.ToString(true));
        }
    }

    internal static class DirectoryInfoExtensions
    {
        public static XElement ToXElement(this System.IO.DirectoryInfo info)
        {
            var element = new XElement("Directory",
                new XAttribute("path", info.FullName),
                new XAttribute("exists", info.Exists.ToString())
                );

            if (info.Exists)
            {
                element.AddAttribute("attributes", info.Attributes.ToString());
                element.AddAttribute("name", info.Name);
            }

            return element;
        }
    }

    internal static class FileInfoExtensions
    {
        public static XElement ToXElement(this System.IO.FileInfo info)
        {
            var element = new XElement("File",
                new XAttribute("path", info.FullName),
                new XAttribute("exists", info.Exists.ToString())
                );

            if (info.Exists)
            {
                element.AddAttribute("attributes", info.Attributes.ToString());
                element.AddAttribute("name", info.Name);
            }

            return element;
        }
    }
}
